feat(cli): olares-cli profile + files (ls/upload/download/cat/rm) + skills#2949
feat(cli): olares-cli profile + files (ls/upload/download/cat/rm) + skills#2949
Conversation
Introduce the multi-profile credential subsystem for olares-cli, covering
the "operate on behalf of a user" scenario via password login and refresh
token import, while keeping the existing kubeconfig-based commands
(osinfo/os/node/gpu/amdgpu/disk) untouched and orthogonal.
New packages:
- pkg/cliconfig: ~/.olares-cli/{config.json,tokens.json} layout, file
perms (0700/0600), atomic write, MultiProfileConfig with current /
previous profile pointers, FindByOlaresID, etc.
- pkg/olares: olaresId -> auth/vault/desktop URL derivation (single
source of truth for all per-profile URLs).
- pkg/auth: stateless protocol layer.
* login.go (mode A): /api/firstfactor with optional TOTP, password
salting, cookie-jar HTTP client.
* refresh.go (mode B): one-shot /api/refresh exchange used to bootstrap
an access token from a user-supplied refresh token.
* token_store.go: plaintext-JSON TokenStore (Phase 1) with Get / Set /
Delete / List / MarkInvalidated. StoredToken includes InvalidatedAt
(Phase 1 defines, Phase 2 writes on /api/refresh 401/403); Set()
defensively zeroes it on every fresh grant.
* jwt.go: ExpiresAt(token) only - no signature verification, no other
claims exposed (username/groups/mfa/jid are explicitly untrusted).
- pkg/credential: Provider chain + ResolvedProfile.
* DefaultProvider resolves Profile + token, returning typed errors with
invalidated > expired > ok priority (ErrTokenInvalidated /
ErrTokenExpired / ErrNotLoggedIn), each with a "run profile login"
CTA. Profile config is never silently mutated.
* EnvProvider stub for future Phase 3 in-cluster scenario.
- pkg/cmdutil/Factory: lazy DI for Credential / ResolvedProfile / HTTP
client (auto-injects Authorization: Bearer; no auto-refresh in Phase 1).
New commands (cmd/ctl/profile, no separate `auth` namespace):
- profile list / use / remove / login / import.
- login & import auto-create the profile on first use, reuse-and-overwrite
when the existing token is missing/expired/invalidated, and reject only
when a still-valid token is present (with a profile-remove hint).
- list shows STATUS: logged-in (Xh) / expired / invalidated / never.
Also:
- Register profile command in cmd/ctl/root.go.
- Ignore cli/docs/ (design notes are local-only, not part of the
shipping repo).
Phase 1 trade-offs (deferred to Phase 2): OS keychain backend, automatic
refresh-with-lock (sync.Map + flock + double-check), LarePass OAuth
device-flow login, wizard activation -> profile bridge.
Made-with: Cursor
Closes the Phase 1 loop with a first authenticated command and rolls in the audit fixes from the cleanup pass. `files ls <fileType>/<extend>[/<subPath>]`: - Parse + validate the 3-segment front-end path used by files-backend (drive/Home, drive/Data, sync/<repo>, awss3/<account>/<bucket>, ...); unknown fileType / bad drive extend are rejected client-side before any HTTP. - Percent-encode each segment via url.PathEscape (mirrors the web app's encodeUrl) so filenames with `#`, `?`, `+`, spaces, `%`, non-ASCII survive the trip to /api/resources. - Render a one-line header (`<path> (N dirs, M files, modified ...)`) followed by a MODE / SIZE / TYPE / MODIFIED / NAME table; MODE decodes os.FileMode (drwxr-xr-x / -rw-r--r-- / Lrwxr-xr-x) and TYPE surfaces the backend's class (video / image / audio / pdf / text / blob / ...). `--json` passes the raw response through verbatim. - 401/403 reuses DefaultProvider's "run profile login" CTA; other non-2xx surface the backend's error/code+message JSON verbatim. Supporting plumbing: - olares.ID.FilesURL derives `https://files.<terminusName>` (matches the web app's getModuleSever('files')). - ResolvedProfile + DefaultProvider expose FilesURL so commands don't have to redo the derivation. - Factory's HTTP client now injects the access token via the custom `X-Authorization` header (was `Authorization: Bearer`). Confirmed via l4-bfl-proxy + BFL filters that the standard Authorization header is filtered at the edge and never reaches per-user services; the web app uses the same X-Authorization path. - Root command grows a persistent `--profile` flag bound straight onto the shared Factory.ProfileOverride, so any subcommand that calls factory.ResolveProfile honors it without re-declaring the flag. - Drop the unused Factory.Stderr field. .gitignore: exclude the local `/files/` and `/market/` checkouts that are kept beside the repo for cross-reference but must never be committed. Tests: ParseFrontendPath (15 cases incl. URL-escape table covering `#?+%` / spaces / non-ASCII), formatSize (10 cases incl. 1023/1024/1MB boundary), formatMode (incl. the live-observed dir mode 2147484141 → drwxr-xr-x), formatType, formatHTTPError (401/403, error/code+message, raw fallback), renderListing (header + counts + dirs-first sort). Made-with: Cursor
Replaces the Phase 1 plaintext ~/.olares-cli/tokens.json store with a
per-OS keychain backend so refresh tokens no longer sit on disk in clear
text. The auth.TokenStore surface is unchanged; production code now
constructs a keychainStore via auth.NewTokenStore, while tests inject an
in-memory fake through auth.NewTokenStoreWith.
Backends (cli/internal/keychain):
- darwin: system Keychain for the master key, AES-256-GCM file blobs
under StorageDir; falls back to a file-only master key if the system
keychain is unavailable (sandbox / CI).
- linux/other: file-based master key + AES-256-GCM blobs under
StorageDir, all 0600/0700.
- windows: DPAPI-encrypted values under HKCU\Software\OlaresCli\keychain.
Hardening / UX in the same change:
- keychainStore.List tolerates a single corrupted entry (warn to stderr,
skip) instead of aborting the whole `profile list`.
- StorageDir falls back to os.TempDir with a stderr warning when
UserHomeDir is unresolvable, so we never silently write to '/'.
- keychain.Backend(service) reports the active backend label
(system-keychain / file-fallback / file / registry+dpapi); printed
after every successful login/import so users notice when they land
on the file fallback.
- keychain.PurgeService is invoked when the last profile is removed,
cleaning up the master key + storage dir / registry subkey so we
don't leave orphan secrets behind.
- wrapError stays terse by default and only attaches the verbose
troubleshooting hint when OLARES_CLI_DEBUG is set.
Refactor:
- AES-GCM constants and the safeFileName helper live in a single
aesgcm.go (//go:build !windows) so darwin and linux can't drift on
the on-disk envelope.
- The in-memory keychain fake is promoted to its own keychainfake
subpackage and shared by pkg/auth and cmd/ctl/profile tests.
Docs in cli/internal/keychain/doc.go explain the deltas vs. lark-cli's
upstream copy, including why olares-cli keeps the KeychainAccess
interface + keychainfake (keychainStore has MarkInvalidated / List /
InvalidatedAt semantics that need unit-test coverage).
Made-with: Cursor
Restructure cli/pkg/auth so it mirrors the two-tiered authentication
pattern in apps/packages/app/src/utils/{account.ts,BindTerminusBusiness.ts}
1:1, and migrate the wizard package off its own duplicated copies of the
salt math, cookie jar, and 2FA wiring.
- pkg/auth.LoginRequest: drop the hallucinated TargetURL/SkipSecondFactor
fields; add NeedTwoFactor (controls vault vs desktop targetURL only)
and AcceptCookie (1:1 with TS `acceptCookie` arg). Docstrings cite the
TS file + line numbers so the next change does not have to reverse-
engineer ground truth.
- pkg/auth.FirstFactor: new exported low-level primitive that POSTs
/api/firstfactor and returns the raw token without inspecting `fa2`,
matching TS onFirstFactor (account.ts L7-71).
- pkg/auth.Login: rewritten as a thin wrapper around the shared
firstFactorWithClient + optional /api/secondfactor/totp escalation,
matching TS loginTerminus (BindTerminusBusiness.ts L353-446). Gate
uses tok.FA2 only, deliberately diverging from TS's
`tok.FA2 || needTwoFactor` because the CLI has no caller-side
knowledge to defensively force 2FA and OR'ing would surface a
spurious ErrTOTPRequired the moment a caller probes with the desktop
targetURL.
- pkg/wizard.UserBindTerminus: switch from auth.Login to auth.FirstFactor
with NeedTwoFactor=false / AcceptCookie=false, mirroring TS L58-66.
This restores `olares-cli wizard activate`, which broke when the
unified auth.Login enforced a 2FA gate the original wizard flow
intentionally bypassed (no MFA seed exists at signup time).
- pkg/wizard.LoginTerminus: pass NeedTwoFactor through and set
AcceptCookie=true to match TS L364-372; keep the eager-TOTP and
ErrTOTPRequired retry paths that bridge the CLI to the MFA-store
TOTP source.
- profile login: pass NeedTwoFactor=true so /api/firstfactor uses the
desktop targetURL and Authelia honestly reports fa2=true on
2FA-enabled accounts. With the previous vault targetURL the server
silently downgraded to fa2=false and the TOTP prompt never fired.
Made-with: Cursor
Adds four new verbs to `olares-cli files`, mirroring the LarePass web app's
Drive UX over the per-user files-backend on `files.<terminusName>`:
- `files upload` — resumable chunked upload (Drive v2 protocol: upload-link
+ file-uploaded-bytes + Content-Range POSTs). Recurses into local
directories; the destination directory must pre-exist on the server because
POST /api/resources/<dir>/ auto-renames an existing dir to "Dir (1)" instead
of returning 409.
- `files download` — single-file with server-driven `Range:` resume + atomic
`.tmp`+rename overwrite, or recursive directory mirroring with
`--parallel N` errgroup-bounded concurrency.
- `files cat` — streams `/api/raw/<path>?inline=true` to stdout, refusing
directories up-front for a friendlier message than the backend's terse 400.
- `files rm` — Unix-like `-r/-R` and `-f/--force`; groups targets by
parent dir and issues one `DELETE /api/resources/<parent>/` per group with
`{"dirents":[...]}` in the body, matching the frontend's batchDelete wire.
`Stat` uses the parent-listing strategy (list parent dir, find leaf in items)
rather than `GET /api/resources/<file>` directly, because the backend's
single-file List handler hard-codes `Content: true` and 500s on most real
files. This matches what the LarePass web app does.
All four commands reuse the existing 3-segment `FrontendPath` parser and
the `X-Authorization` access-token transport from `pkg/cmdutil/factory.go`.
httptest-driven unit tests cover resume / overwrite / retries / range
headers (download), plan grouping + dir-without-r refusal + wire shape (rm),
encodeURIComponent parity + chunked upload protocol (upload).
Made-with: Cursor
Two new SKILL.md files under cli/skills/, modeled after larksuite-cli's lark-shared / lark-drive style, so AI coding assistants get reliable guidance on: - olares-shared: profile model, password+TOTP login (mode A), refresh-token import (mode B), profile use/list/remove + global --profile, OS keychain storage layout, the re-authentication state machine, and a recovery table for HTTP 401/403 / already-authenticated / 2FA-required errors. - olares-files: the 3-segment frontend path schema, trailing-slash conventions, two server-side quirks the agent MUST respect (POST mkdir auto-renames existing dirs to "Foo (1)"; GET single-file resource returns HTTP 500 — Stat must list parents instead), X-Authorization transport, and per-verb cheatsheets for ls / upload / download / cat / rm with wire shapes and key flags. The frontmatter follows the lark-cli schema (name / version / description with trigger keywords / metadata.requires.bins / metadata.cliHelp), so Cursor's skill loader can pick them up from cli/skills/<name>/SKILL.md. Made-with: Cursor
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
- Re-stat the local file before each download attempt in --resume mode so Range: bytes=N- tracks partial progress after a failed append; accumulate per-attempt bytes for the success return and align error returns with that accounting. - Add TestDownloadFile_ResumeRetryRefreshesRange (partial 206 + cut, then second GET with bytes=6-). - Drop redundant atomic adds under mutex in files upload/download commands; use formatBytes from ls.go and remove duplicate humanBytes. Made-with: Cursor
| framework/app-service/bin | ||
|
|
||
| /files/ | ||
| /market/ |
There was a problem hiding this comment.
Gitignore entries appear accidentally committed for local dirs
Low Severity
The /files/ and /market/ entries at the repository root gitignore look like developer-local directory exclusions (likely locally-cloned sibling repos) rather than project-wide patterns. No such directories exist in the repository structure. Committing these pollutes the shared .gitignore with entries that only serve one developer's workspace layout.
Reviewed by Cursor Bugbot for commit f8cdfdc. Configure here.
Bugbot: FrontendPath.URLPath used url.PathEscape per segment while download/cat/rm/upload use upload.EncodeURL, diverging for '+', '!*()', etc. Delegate URLPath to EncodeURL(p.String()) and extend path tests (report (1).txt, x+y → x%2By). Update ls command comment. Made-with: Cursor
Move EncodeURL / EncodeURIComponent out of package upload into a small shared package so download, rm, path, and upload all depend on the same wire encoder without cmd/pkg layers importing upload only for URLs. - Add cli/pkg/files/encodepath with tests (former upload/encode*.go). - Update upload api/uploader, download client, rm, FrontendPath.URLPath. - Clarify download/list.go is for the download walker, not `files ls`. Made-with: Cursor
| .vscode | ||
| .DS_Store | ||
| cli/output | ||
| cli/docs/ |
There was a problem hiding this comment.
Gitignore prevents PR deliverable docs from being tracked
Medium Severity
The new cli/docs/ gitignore entry prevents all files under that directory from being committed, but the PR description explicitly lists cli/docs/notes/auth-2fa-semantics.md as a deliverable documenting the 2FA authentication semantics. This file would never be tracked by git.
Reviewed by Cursor Bugbot for commit 4ad2cad. Configure here.
Relocate encodepath, download, rm, and upload from cli/pkg/files to cli/internal/files so they are clearly olares-cli implementation details (not a stable public library surface). The legacy root package cli/pkg/files (installer URLs, rate limiter, etc.) stays in pkg for existing importers (storage, bootstrap, terminus, ...). Update `files` Cobra imports and olares-files SKILL links. Made-with: Cursor
|
Branch status: Recent follow-ups since the original description:
Tests run locally: |
Add a profile-authenticated `market` command tree that talks to the per-user Market app-store v2 API (`<MarketURL>/app-store/api/v2`) using the same `cmdutil.Factory` / `X-Authorization` transport as `olares-cli files`. The existing `cli/cmd/ctl/app/` tree is kept intact so reviewers can diff the two side by side. Verbs: - Catalog (read-only): `list`, `categories`, `get`. - Runtime: `status` with the "not installed" UX fix, the source-fallback hint, and `--watch` for op-agnostic recovery (e.g. confirming a previously fire-and-forget install reaches `running`). - Lifecycle (mutating, all support `--watch`): `install`, `upgrade`, `uninstall`, `clone`, `stop`, `resume`, `cancel`. - Local sources: `upload` / `delete`, restricted to `-s upload|studio|cli`. The `--watch` machinery (`cli/cmd/ctl/market/watch.go`) blocks until the backend reaches a terminal state. Per-op success/failure sets are derived from the backend's `ApplicationManagerState` enum; `matchOpType` gates "success" against the in-flight `OpType` so an `upgrade` issued on an already-`running` app cannot return success on tick zero. `cancel` and `status --watch` are deliberately op-agnostic. Output: TTY emits one info line per state transition; `-o json` emits a single final `OperationResult` with the new `finalState` / `finalOpType` fields (`omitempty`, so non-watch JSON output is byte-identical). Ctrl-C exits cleanly via `signal.NotifyContext` with `watch canceled by user`; the underlying mutation is not stopped — re-attach via `status --watch`. Plumbing: - `cli/pkg/olares.ID.MarketURL(localPrefix)` derives the market origin in the same shape as the existing `FilesURL` / `DesktopURL` / `VaultURL`. - `credential.ResolvedProfile.MarketURL` and `buildResolved` propagate it through the factory, so commands never touch kubeconfig. - `cmd/ctl/root.go` registers `market.NewMarketCommand(factory)` next to the existing `app` / `profile` / `files` commands. Skill: - `cli/skills/olares-market/SKILL.md` mirrors the `olares-files` / `olares-shared` shape (frontmatter, top callout, core concepts → auth transport → command cheatsheet → `--watch` section → errors → workflows → security rules). Defers profile / login / 401-403 recovery to `olares-shared` instead of duplicating it. Tests: `cli/cmd/ctl/market/watch_test.go` covers the classifier (per-op terminal sets, OpType gating, `cancel` op-agnostic path, op-agnostic `status --watch`) and end-to-end `waitForTerminal` against an `httptest.Server` (state transitions, timeout surfacing last-seen state, JSON output stability). Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
There are 6 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b3ba004. Configure here.
| return "" | ||
| } | ||
| fieldName := path[len(path)-1] | ||
| i18nPath := append(path[:len(path)-1], "i18n") |
There was a problem hiding this comment.
Slice append mutates caller's variadic path argument
Medium Severity
resolveI18nField uses append(path[:len(path)-1], "i18n") which overwrites the last element of the original path slice's backing array, since the sub-slice has spare capacity. While fieldName is captured before the mutation, the path slice passed by the caller is corrupted. When called from printAppDetail with a literal variadic like "app_info", "app_entry", "i18n", "title", Go allocates a fresh backing array per call so it's safe today — but if path were ever reused or if the compiler optimized the allocation, the last element would be silently replaced with "i18n". The idiomatic fix is to copy the slice before appending.
Reviewed by Cursor Bugbot for commit b3ba004. Configure here.
| task.RelativePath, uploaded, total, | ||
| formatBytes(uploaded), formatBytes(total)) | ||
| } | ||
| } |
There was a problem hiding this comment.
Progress callback closure captures shared mutable variable
Low Severity
The lastReported variable inside the progress closure is local to each goroutine, so it works correctly for throttling. However, the fmt.Fprintf(out, ...) calls from multiple goroutines writing to the same out writer concurrently have no synchronization. With --parallel > 1, interleaved writes to out can produce garbled output lines. The same issue exists in download.go's runDownloadDir.
Reviewed by Cursor Bugbot for commit b3ba004. Configure here.
…to cli/profile-auth-phase1 * 'cli/profile-auth-phase1' of github.com:beclab/olares: feat(cli): add olares-cli market commands with --watch + skill


Summary
Bring
olares-clito a usable baseline for profile-based authentication, end-to-end Drive file management, and the per-user Market app-store, plus ship Cursor agent skills so AI assistants can drive the CLI safely.All changes are confined to
cli/(includingcli/skills/). The branch stacks the original feature commits plus follow-ups (Bugbot / encoding / package layout / market). High-level breakdown:1. Profile / auth subsystem (
b9a7bde0,ce392322,53c49514)olares-cli profilecommand group:login(password + TOTP),import(refresh-token bootstrap),list,use,remove, plus a global--profileflag.cli/pkg/authpackage wraps AutheliafirstFactor+loginTerminuswith the same shape as the LarePass TS client, including correct 2FA semantics:tok.FA2is the only server-driven gate that escalates to a TOTP-bearing second login.LoginRequest.NeedTwoFactoris only for activation/reset flows that need to switchtargetURL; it must not be set during normal interactive login.performPasswordResetnow mirrors the TS sequence: pre-reset login (no 2FA) →ResetPassword→ post-reset login (with 2FA).cli/docs/notes/auth-2fa-semantics.md.OlaresCli).~/.local/share/olares-cli/.HKCU\Software\OlaresCli\keychain.cli/pkg/authinstead of duplicating login/refresh logic.2.
olares-cli filescommands (5efe2a0d,f5aad739+ follow-ups)ls: directory listing viaGET /api/resources/<encPath>/.FrontendPath.URLPath()uses the same JSencodeUrlsemantics as the other verbs (sharedencodepathpackage) sols/download/cat/rm/uploadagree on wire paths for names with+,!*'(), spaces, etc.upload: resumable, chunked upload that ports the LarePass Resumable.js protocol to Gocli/internal/files/upload— directory-awareBuildPlan, server-driven resume via/upload/file-uploaded-bytes/, and file-level concurrency. Path / query encoding usescli/internal/files/encodepath(JSencodeURIComponentparity).POST /api/resources/<dir>/auto-renames an existing dir toFoo (1). The CLI therefore does not pre-mkdirthe destination root or empty subdirs — it relies on implicit creation during chunk POSTs.download,cat,rm(cli/internal/files/download,cli/internal/files/rm):download: file or recursive directory; resume viaRange, atomic.tmp+ rename for full downloads, retry on transient 5xx. Resume retries re-read the local file size before each attempt so a partial append cannot re-send a staleRange:header.cat: streams a single file's bytes to stdout viaGET /api/raw/....rm: Unix-like UX (-r,-f, multi-arg, TTY confirmation), batched per-parentDELETE /api/resources/<parent>/with{ "dirents": [...] }.GET /api/resources/<file>(no trailing slash) returns HTTP 500 because the handler tries to embed file content.Statmirrors the web app — list the parent dir and look the basename up initems.cli/cmd/ctl/files/root.goregisters all five verbs.cli/internal/files/{upload,download,rm,encodepath}/andcli/cmd/ctl/files/(parsing, transfer, retry/resume, delete planning / wire shape).Package layout: Drive-specific libraries live under
cli/internal/files/(encodepath,upload,download,rm) so they are not mistaken for a stable public API. The legacy rootcli/pkg/files(installer download URLs, rate limiter, etc.) remains inpkgfor existing bootstrap / storage / terminus importers.3.
olares-cli marketcommands (b3ba0047)New
marketcommand tree undercli/cmd/ctl/market/that talks to the per-user Market app-store v2 API at<MarketURL>/app-store/api/v2. It uses the samecmdutil.Factory+X-Authorizationtransport asfiles, so it does not depend on kubeconfig. The existingcli/cmd/ctl/app/tree is kept intact so reviewers can diff the two side by side.list(with-ccategory filter and-aall-sources),categories,get.statuswith the "not installed" UX fix and the source-fallback hint (App is installed under source 'Y' (not 'X')); supports--watchfor op-agnostic recovery (confirming a previously fire-and-forget install reachesrunning).--watch):install,upgrade,uninstall,clone,stop,resume,cancel. Per-verb gotchas:clone --titleis required (≤ 30 chars),uninstall --cascade/--delete-data,cancelis op-agnostic.upload(single file or directory of.tgz/.tar.gz) anddelete, restricted to-s upload|studio|cli.--watchmachinery (cli/cmd/ctl/market/watch.go):GetMarketStateevery--watch-interval(default2s) until terminal state, bounded by--watch-timeout(default15m).ApplicationManagerStateenum (framework/app-service/api/app.bytetrade.io/v1alpha1/appmanager_states.go).matchOpTypegates "success" against the in-flightOpTypeso anupgradeissued on an already-runningapp cannot return success on tick zero.cancelandstatus --watchare deliberately op-agnostic.signal.NotifyContextcleans up Ctrl-C withwatch canceled by user. The underlying mutation is not stopped — re-attach viastatus --watch.-o jsonemits a single finalOperationResultwith newfinalState/finalOpTypefields (omitempty, so non-watch JSON output is byte-identical);-qsuppresses transitions but exit code still propagates.Plumbing:
cli/pkg/olares.ID.MarketURL(localPrefix)deriveshttps://market.<localPrefix><terminusName>in the same shape as the existingFilesURL/DesktopURL/VaultURL.credential.ResolvedProfile.MarketURLandbuildResolvedpropagate it through the factory.cmd/ctl/root.goregistersmarket.NewMarketCommand(factory)next toapp/profile/files.Tests:
cli/cmd/ctl/market/watch_test.gocovers the classifier (per-op terminal sets, OpType gating,cancelop-agnostic path, op-agnosticstatus --watch) and end-to-endwaitForTerminalagainst anhttptest.Server(state transitions, timeout surfacing last-seen state, JSON output stability).4. Agent skills (
68fa9424,b3ba0047)cli/skills/olares-shared/SKILL.md: profile model, login modes, keychain layout, re-auth state machine, error → recovery table, security rules.cli/skills/olares-files/SKILL.md: 3-segment frontend path, trailing-slash conventions, the two server quirks above,X-Authorizationtransport, per-verb cheatsheet forls/upload/download/cat/rm(links point atcli/internal/files/...where relevant).cli/skills/olares-market/SKILL.md: source resolution (market.olarescatalog vscli/upload/studiolocal sources), the install/upgrade/uninstall/stop/resume/cancel state machine, OpType-vs-State race-safety, fullmarketcheatsheet, dedicated--watchsection with per-op terminal-set table, op-agnosticstatus --watchrecovery, mermaid flowchart, errors → fixes, typical workflows, and security rules. Defers profile / login / 401-403 recovery toolares-shared.name/version/description/metadata.requires.bins/metadata.cliHelp) so Cursor's skill loader can pick them up.Test plan
cd cli && go build ./... && go vet ./...cd cli && go test ./pkg/auth/... ./internal/files/... ./cmd/ctl/files/... ./cmd/ctl/market/...olares-cli profile loginagainst a real Olares instance with TOTP enabled — verify the prompt order matches the TS client (password → TOTP only whenfa2is true).olares-cli profile importfrom a refresh token, thenprofile list/profile use.olares-cli files ls drive/Home/.olares-cli files upload ./somefile drive/Home/Documents/(existing dir → noDocuments (1)should appear) and same for a directory.olares-cli files download drive/Home/Documents/somefile ./outand... -r drive/Home/Documents/ ./outdir.olares-cli files cat drive/Home/Code/opencode.json(the original HTTP 500 repro — should now stream).olares-cli files rm drive/Home/tmp/foo.txtand... -r drive/Home/tmp/.olares-cli market list/... categories/... get firefoxagainst a live cluster.olares-cli market install firefox --watchblocks untilrunning, prints transitions, exit 0.olares-cli market install nonexistent-app --watchexits non-zero withinstallFailedordownloadFailed.olares-cli market install firefoxthenolares-cli market status firefox --watchreachesrunning.olares-cli market upgrade firefox --version <newer> --watchwaits past the stale-OpType tick before declaring success.olares-cli market clone firefox --title "Firefox (work)" --watchlands onrunning.olares-cli market stop firefox --watchthen... resume firefox --watch.olares-cli market cancel firefox --watchagainst an in-flight install lands oninstallingCanceled.Ctrl-Cmid-watch produceswatch canceled by userand non-zero exit;status --watchre-attaches cleanly.olares-cli market upload ./myapp-1.0.0.tgzthen... install myapp -s cli --watch.cli/skills/*/SKILL.mdare picked up by Cursor (skill list showsolares-shared/olares-files/olares-market).Made with Cursor
Note
Medium Risk
Adds substantial new CLI surfaces that perform remote file transfer/deletion and app lifecycle operations; mistakes in path handling, retries/resume, or watch state classification could lead to data loss or confusing/incorrect command outcomes.
Overview
Adds a new
olares-cli filessubtree with front-end path parsing and URL encoding plus verbs for directory listing, resumable chunked uploads, resumable/parallel downloads (including recursive directory pulls), binary-safecat, and Unix-style remote deletion with-r/-fconfirmation and batched deletes.Adds a new profile-based
olares-cli marketsubtree that talks to the per-user app-store v2 API, including catalog queries (list,categories,get), lifecycle actions (install,upgrade,uninstall,clone,stop,resume,cancel), local chartupload/delete, and a shared--watchpoller that waits for terminal states and surfaces final state/optype in structured output.Minor repo hygiene updates include ignoring generated
cli/docs/and top-level/files/and/market/directories in.gitignore.Reviewed by Cursor Bugbot for commit eb951e7. Bugbot is set up for automated code reviews on this repo. Configure here.